Многие считают, что датавиз очень похож на рисование, но я бы больше сравнил это с конструктором LEGO. Каждый график - комбинация слоёв, геометрий и декоративных элементов тем. Но если нет материалов (данных), то каким бы замечательным визуализатором вы ни были, ничего не выйдет.
Сегодня мы разберём технические особенности визуализации в
ggplot2 на данных об алкогольных предпочтениях россиян.
Основной пример в визуализации сегодня будет связан с данными об алкогольных предпочтениях.
В идеале каждый график рассказывает какую-то историю. И хорошо, если эта история сложнее, чем стишок про то, как “муха села на варенье…”. Наверняка в ваших любимых книгах есть не один слой смысла а несколько, поэтому вы их и любите. В приведённой столбиковой диаграмме слой смысла ровно один поэтому в современной аналитике подача информации может быть менее примитивной. В конце концов, многие восхищаются иллюстрациями журнала The Economist, но когда дело доходит до собственных визуализаций всё деградирует до столбиков с односложным посылом. # Делаем график про алкогольные предпочтения ### Загрузка библиотек и шрифтов
library(tidyverse) # Основная библиотека в экосистеме
library(showtext) # Рендер текста
library(ggtext) # Рендер текста
library(sysfonts) # Загрузка шрифтов
library(ggridges) # Красивые геометрии
library(formattable) # Для визуализации таблички
library(viridis) # Цветовые палитры
library(rcartocolor) # Цветовые палитры
# Для работы с картами:
library(sf)
library(terra)
library(tidyterra)
showtext_auto()
# Загружаем шрифт:
GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-Regular.otf', write_disk('HSESans-Regular.otf', overwrite = T))
GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-Bold.otf', write_disk('HSESans-Bold.otf', overwrite = T))
GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-Italic.otf', write_disk('HSESans-Italic.otf', overwrite = T))
GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-SemiBold.otf', write_disk('HSESans-SemiBlod.otf', overwrite = T))
font_add(family = 'HSE Sans',
regular = "HSESans-Regular.otf",
bold = 'HSESans-Bold.otf',
italic = 'HSESans-Italic.otf',
bolditalic = 'HSESans-SemiBlod.otf'
)
Моя большая признательность за данные проекту RLMS HSE. В рамках
добросовестного использования прилагаю ссылку на источник: Российский
мониторинг экономического положения и здоровья населения НИУ ВШЭ (RLMS
HSE)», проводимый Национальным исследовательским университетом “Высшая
школа экономики” и ООО «Демоскоп» при участии Центра народонаселения
Университета Северной Каролины в Чапел Хилле и Института социологии
Федерального научно-исследовательского социологического центра РАН.
(Сайты обследования RLMS HSE: http://www.hse.ru/rlms и https://rlms-hse.cpc.unc.edu).
Надеюсь, что ко мне не будет претензий за то, что я выкладываю
обработанные данные RLMS для этого обучающего поста, на всякий случай
напоминаю, что полные версии данных с обследованиями индивидов находятся
на: https://www.hse.ru/rlms/spss.
df <- read.csv("https://raw.githubusercontent.com/ETymch/Econometrics_2023/refs/heads/main/Datasets/tutorial_gg_rlms.csv")
df %>%
select(bb_age, name) %>%
head(14) %>%
formattable(align =c("c","l"),
list(`Возраст`= color_tile('transparent', 'lightpink')
))
geom_pointdf %>%
ggplot(aes(x = Возраст, y = Напиток)) +
geom_point()
geom_boxplotdf %>%
ggplot(aes(x = Возраст, y = Напиток)) +
geom_boxplot(outlier.shape = NA, coef = 0)
geom_jitterdf %>% ggplot(aes(x = Возраст, y = Напиток)) +
geom_boxplot(outlier.shape = NA, coef = 0) +
geom_jitter()
alpha = [0,1]df %>%
ggplot(aes(x = Возраст, y = Напиток)) +
geom_boxplot(outlier.shape = NA, coef = 0) +
geom_jitter(alpha = 0.2)
В шапке добавим aes(color =Какая-то переменная)`.
df %>%
ggplot(aes(x = Возраст, y = Напиток, color = Возраст)) +
geom_boxplot(outlier.shape = NA, coef = 0) +
geom_point(position=position_jitterdodge(0.2), alpha = 1)
Функции плотности распределения: ggridges.
зlot <- df %>%
ggplot(aes(x = Возраст, y = Напиток, color = Возраст)) +
geom_point(position=position_jitterdodge(0.2), alpha = 0.5) +
ggridges::geom_density_ridges()
plot
viridisplot <- plot + scale_color_viridis_c()
plot
plot +
theme_classic()
plot <- plot + theme_minimal()
plot
Единый шрифт добавляется при помощи включения
base_family, base_size в настройках темы. Шрифты
загружаются и рисуются при помощи библиотек
sysfonts, showtext и ggtext. Последняя
добавляет html инструменты для управления шрифтами.
plot <- plot + theme_minimal(base_family = 'HSE Sans', base_size = 14)
plot
Или изменить уже существующую: +theme(). Также я изменил
формат легенды при помощи guides().
plot <- plot +
theme("Здесь можно менять: формат заголовков, шкал, осей и вообще всего, что можно предстваить.") +
guides(color = guide_colorbar(barwidth = 12, barheight = 0.5))
plot
Для удобства создадим отдельную табличку anno с данными
для аннотации.
anno <- df %>% count(Напиток) # Табличка
plot <- plot +
annotate('text', # Аннотация может быть не только текстом, но и линией, линией со стрелкой, прямоугольником и многим другим.
x = 95, y = anno$Напиток, # Координаты аннотаций.
label = paste0('n = ', anno$Напиток), # формат аннотации
vjust = -0.8) # Сдвинуть по вертикали вниз на 0.8
plot
Добавляем заголовки labs, модифицируем тему. Привожу
полную версию кода.
df %>%
ggplot(aes(x = Возраст, y = Напиток, color = Возраст)) +
geom_point(position=position_jitterdodge(0.2), alpha = 0.15) +
ggridges::geom_density_ridges(fill = '#442F3D', quantile_lines = T, quantiles = 2, color = 'grey20', linewidth = 0.5, alpha = 0.2) +
annotate('text', x = 95, y = anno$Напиток, label = paste0('n = ', anno$Напиток), vjust = -0.8) +
theme_minimal(base_family = 'HSE Sans', base_size = 14) +
theme(legend.position = 'bottom',
legend.margin=margin(t = -0.2, unit='cm'),
panel.grid.minor.x = element_blank(),
panel.grid.minor.y = element_blank(),
panel.grid.major.y = element_blank(),
axis.title.y = element_blank(),
legend.title = element_blank(),
plot.title.position = 'plot',
plot.caption = element_text(size = 7, face = 'italic'),
plot.subtitle = element_markdown(hjust = 0.0, size = 13),
plot.title = element_markdown(hjust = 0.0,
size = 18, face = 'bold')
) +
guides(color = guide_colorbar(barwidth = 12, barheight = 0.5)) +
scale_fill_carto_c(palette = "TealRose", direction = -1) +
scale_color_carto_c(palette = "TealRose", direction = -1) +
labs(x = 'Возраст, лет',
title = 'Каждому возрасту - свой напиток',
subtitle = "<span style = 'color:#D98994;'>Более молодые</span> предпочитают коктейли или пиво,<br> <span style = 'color:#009392;'> познавшие жизнь </span>- водку, самогон и вино.",
caption = 'Данные: Российский мониторинг экономического положения и здоровья населения НИУ ВШЭ (RLMS HSE), http://www.hse.ru/rlms')
Мне эта версия нравится даже больше, потому что информация о возрасте не дублируется.
df %>%
ggplot(aes(x = Возраст, y = Напиток, color = factor(Пол, levels = c(1,2)))) +
geom_point(position=position_jitterdodge(0.5), alpha = 0.25) +
ggridges::geom_density_ridges(fill = '#442F3D', quantile_lines = T, quantiles = 2, color = 'grey20', linewidth = 0.5, alpha = 0.15) +
annotate('text', x = 95, y = anno$Напиток, label = paste0('n = ', anno$Напиток), vjust = -0.8) +
theme_minimal(base_family = 'HSE Sans', base_size = 14) +
theme(legend.position = 'none',
legend.margin=margin(t = -0.2, unit='cm'),
panel.grid.minor.x = element_blank(),
panel.grid.minor.y = element_blank(),
panel.grid.major.y = element_blank(),
axis.title.y = element_blank(),
legend.title = element_blank(),
plot.title.position = 'plot',
plot.caption = element_text(size = 7, face = 'italic'),
plot.subtitle = element_markdown(hjust = 0.0, size = 13),
plot.title = element_markdown(hjust = 0.0,
size = 18, face = 'bold')
) +
scale_fill_manual(values = c('#E39921', '#E482B5')) +
scale_color_manual(values = c('#E39921', '#E482B5')) +
labs(x = 'Возраст, лет',
title = 'Каждому - свой напиток',
subtitle = "<span style = 'color:#E482B5;'>Женщины </span>заметно чаще <span style = 'color:#E39921;'>мужчин</span> выбирают вино и коктейли.<br><span style = 'color:#E482B5;'>Женщины</span> начинают употреблять самогон в более позднем возрасте, чем <span style = 'color:#E39921;'>мужчины</span>.",
caption = 'Данные: Российский мониторинг экономического положения и здоровья населения НИУ ВШЭ (RLMS HSE), http://www.hse.ru/rlms')
phi4 от Microsoft.Я попросил языковую модель предложить свой вариант визуализации. Её код выглядел так:
df %>%
mutate(age_group = ifelse(Возраст < 40, "<40", ">=40")) %>%
mutate(Пол = ifelse(Пол == 1, 'Мужчины', 'Женщины')) %>%
ggplot(aes(x = Напиток, fill = factor(Пол, levels = c('Мужчины', 'Женщины')))) +
geom_bar(position = position_dodge(), stat = 'count') +
facet_wrap(~age_group) +
theme_minimal() +
scale_fill_manual(values = c('blue', 'pink')) + # неплохое сочетание цветов
labs(title = 'Распределение предпочпочтений по возрастным группам', # Да, именно предпочпочтений😊
x = "Напиток",
y = "Кол-во людей")
Довольно средне, не правда ли? Зная
ggplot2 за минуту этот график можно улучшить для более
приятной версии.
df %>%
mutate(age_group = ifelse(Возраст < 40, "Возраст < 40 лет", " Возраст >= 40 лет") %>% factor(levels = c("Возраст < 40 лет", " Возраст >= 40 лет"))) %>%
mutate(Пол = ifelse(Пол == 1, 'Мужчины', 'Женщины')) %>%
ggplot(aes(x = Напиток, fill = factor(Пол, levels = c('Мужчины', 'Женщины')))) +
geom_bar(position = position_dodge(), stat = 'count', alpha = 0.9) +
facet_wrap(~age_group) + # отличная находка phi4
theme_classic(base_family = 'HSE Sans', base_size = 14) + # Такая тема мне нравится больше
scale_fill_manual(values = c('blue', 'pink')) +
labs(title = 'Распределение предпочтений по возрастным группам',
x = "Напиток",
y = "Кол-во людей") +
coord_flip() + # так намного лучше
theme(legend.position = 'bottom',
legend.title = element_blank(),
axis.title.y = element_blank(),
plot.title.position = 'plot',
plot.title = element_markdown(hjust = 0.0,
size = 16, face = 'bold'))
Мне нравится, что phi4 предложил
разбить график на две клетки, позволил продемонстрировать замечательную
команду facet_wrap(), которая позволяет создавать
композиции из нескольких графиков.
В ванильной ggplot2 около 70 геометрий. Если добавить геометрии
из модификаций, то счёт идёт на сотни. Есть также могучая библиотека
ggforce и многие другие, которые расширяют набор
геометрий.
Можно делать свои шаблоны для графиков.
Удобно автоматизировать.
Можно строить карты.
Можно делать анимации, но их применение в аналитике ограничено.
Библиотека patchwork.
Соединение графиков в ансамбль в одну строчку:
plot_1 + plot_2
Есть более простой способ: добавить +facet_wrap в
ggplot.
Мне очень нравится эта композиция из графиков, потому что эта композиция рассказывает монолитную историю. Как сделать такой график я показывал здесь.
Карту можно построить, написав 2 строчки кода. Например:
kadastr %>%
mutate(`Объект культурного наследия` = ifelse(cultural_heritage_val != '', "Да", 'Нет')) %>%
ggplot() +
geom_sf(aes(fill = `Объект культурного наследия`, color = `Объект культурного наследия`)) +
geom_sf(data = kadastr_na, color = '#4D483E', size = 1, alpha =0.9) +
scale_color_manual(values = c('#D05546', '#4D483E')) +
scale_fill_manual(values = c('#D05546', '#4D483E')) +
theme_void(base_family = 'HSE Sans') +
theme(legend.position = 'bottom',
text = element_text(colour = "white"),
plot.background = element_rect(fill = 'black', color = '#D05546'),
legend.margin = margin(-20,0,0,0, 'pt'),
plot.margin = margin(-16, -16, 0, -16, "pt")
)
Библиотеки: sf, tidyterra,
Можно построить карту Москвы на кадастровых данных, которые в своём обучающем материале собрал Марсель Салихов. Данные, возможно, опубликую в будущем.
kadastr %>% # Табличка
ggplot() + # Строим график
geom_sf(aes(fill = `Объект культурного наследия`, color = `Объект культурного наследия`)) + # слой полигонов
geom_sf(data = kadastr_na, color = '#4D483E', size = 1, alpha =0.9) + # слой точек для домов, которые не измерены
scale_color_manual(values = c('#D05546', '#4D483E')) + # Цветовая шкала color - это, как правило, про контуры объекта
scale_fill_manual(values = c('#D05546', '#4D483E')) + # Шкала заливки, fill - как правило, про внутреннюю заливку объекта.
theme_void(base_family = 'HSE Sans') + # Пустая тема
theme(legend.position = 'bottom',
text = element_text(colour = "white"),
plot.background = element_rect(fill = 'black', color = '#D05546'),
legend.margin = margin(-20,0,0,0, 'pt'),
plot.margin = margin(-16, -16, 0, -16, "pt")
)
Сегодня мы рассмотрели несколько мотивирующих примеров визуализации в
ggplot2. Строить графики - не сложно, в большинстве случаев
можно уложиться в несколько строчек кода. Современные языки
программирования, такие как R, достаточно просты в
освоении, а код на них легко читается и выглядит лаконично. С помощью
кода и инструментов парсинга можно автоматизировать построение графиков
для регулярных отчётов или углубить понимание данных при помощи
комбинирования геометрий и игры с цветом. Спасибо за чтение, приятного
путешествия в мир визуализации данных.